﻿using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading;
using BitsOfStuff.Support;

namespace BitsOfStuff
{
    /// <summary>
    /// Class that provide functions for curved aproximations
    /// </summary>
    public class DouglasPeuckerReduction
    {
        /// <summary>
        /// Uses the Douglas Peucker algorithim to reduce the number of points.
        /// </summary>
        ///<returns>returns bitmap of curved points</returns>
        public static bool[] Result;

        private static HashSet<string> _recursionAlredyRuning;

        private static List<int>[] _parialThreadResult;

        private static IEnumerable<MyPoint> douglasPeuckerReduction(List<MyPoint> points, Double tolerance)
        {
            if (points == null || points.Count < 3)
                return points;

            const int firstPoint = 0;
            _recursionAlredyRuning = new HashSet<string>();
            Int32 lastPoint = points.Count - 1;
            var pointIndexsToKeep = new List<Int32> { firstPoint, lastPoint };

            //Add the first and last index to the keepers

            //The first and the last point can not be the same
            while (points[firstPoint].Equals(points[lastPoint]))
            {
                lastPoint--;
                if (lastPoint < 1)
                    break;
            }

            douglasPeuckerReduction(points, firstPoint, lastPoint, tolerance, ref pointIndexsToKeep, 0);

            pointIndexsToKeep.Sort();

            return pointIndexsToKeep.Select(index => points[index]).ToList();
        }

        /// <summary>
        /// function for basic aproximations
        /// </summary>
        /// <param name="points">Raw points</param>
        /// <param name="tolerance">tolerance</param>
        /// <returns></returns>
        public static List<int> douglasPeuckerReduction(MyPoint[] points, Double tolerance)
        {
            var pointsList = new List<MyPoint>(points);
            var partialResult = douglasPeuckerReduction(pointsList, tolerance);
            return partialResult.Select(myPoint => pointsList.IndexOf(myPoint)).ToList();
        }

        /// <summary>
        /// Function for aproximation that works multithreads
        /// </summary>
        /// <param name="points">Raw points</param>
        /// <param name="tolerance">tolerance</param>
        /// <param name="resultIndex">start index of partial result for this function</param>
        public static void douglasPeuckerReductionThread(MyPoint[] points, Double tolerance, int resultIndex)
        {
            var pointsList = new List<MyPoint>(points);
            var partialResult = douglasPeuckerReduction(pointsList, tolerance);
            _parialThreadResult[resultIndex] = new List<int>();
            foreach (var myPoint in partialResult)
            {
                _parialThreadResult[resultIndex].Add(pointsList.IndexOf(myPoint));
            }
        }

        /// <summary>
        /// Douglases the peucker reduction.
        /// </summary>
        /// <param name="points">The points.</param>
        /// <param name="firstPoint">The first point.</param>
        /// <param name="lastPoint">The last point.</param>
        /// <param name="tolerance">The tolerance.</param>
        /// <param name="pointIndexsToKeep">The point indexs to keep.</param>
        /// <param name="recursionMaxDistance"> </param>
        private static void douglasPeuckerReduction(List<MyPoint> points, Int32 firstPoint, Int32 lastPoint, Double tolerance, ref List<Int32> pointIndexsToKeep, double recursionMaxDistance)
        {
            Double maxDistance = 0;
            Int32 indexFarthest = 0;

            var indexError = new List<MyPoint>();
            // count error for new points - take lower error index

            var distanceList = new List<double>();

            for (Int32 index = firstPoint; index < lastPoint; index++)
            {
                Double distance = perpendicularDistance(points[firstPoint], points[lastPoint], points[index]);
                if (distance > maxDistance)
                {
                    maxDistance = distance;
                    indexFarthest = index;
                }
            }

            // check if current recursion distance is greater than passed through args
            if (maxDistance > recursionMaxDistance) recursionMaxDistance = maxDistance;

            if (maxDistance > (recursionMaxDistance * tolerance) / 100 && indexFarthest != 0)
            {
                //Add the largest point that exceeds the tolerance
                pointIndexsToKeep.Add(indexFarthest);

                var recursionFirst = firstPoint.ToString() + indexFarthest.ToString() + recursionMaxDistance.ToString();
                if (!_recursionAlredyRuning.Contains(recursionFirst))
                {
                    _recursionAlredyRuning.Add(recursionFirst);
                    douglasPeuckerReduction(points, firstPoint, indexFarthest, tolerance, ref pointIndexsToKeep, recursionMaxDistance);
                }
                var recursionSecond = indexFarthest.ToString() + lastPoint.ToString() + recursionMaxDistance.ToString();
                if (!_recursionAlredyRuning.Contains(recursionSecond))
                {
                    _recursionAlredyRuning.Add(recursionSecond);
                    douglasPeuckerReduction(points, indexFarthest, lastPoint, tolerance, ref pointIndexsToKeep, recursionMaxDistance);
                }
            }
        }

        /// <summary>
        /// The distance of a point from a line made from point1 and point2.
        /// </summary>
        /// <param name="point1"> </param>
        /// <param name="point2"> </param>
        /// <param name="point"> </param>
        /// <returns></returns>
        private static Double perpendicularDistance(MyPoint point1, MyPoint point2, MyPoint point)
        {
            //Area = |(1/2)(x1y2 + x2y3 + x3y1 - x2y1 - x3y2 - x1y3)|   *Area of triangle
            //Base = √((x1-x2)²+(x1-x2)²)                               *Base of Triangle*
            //Area = .5*Base*H                                          *Solve for height
            //Height = Area/.5/Base

            Double area = Math.Abs(.5 * (point1.getX() * point2.getY() + point2.getX() * point.getY() + point.getX() * point1.getY() - point2.getX() * point1.getY() - point.getX() * point2.getY() - point1.getX() * point.getY()));
            Double bottom = Math.Sqrt(Math.Pow(point1.getX() - point2.getX(), 2) + Math.Pow(point1.getY() - point2.getY(), 2));
            Double height = area / bottom * 2;

            return height;

            //Another option
            //Double A = Point.X - Point1.X;
            //Double B = Point.Y - Point1.Y;
            //Double C = Point2.X - Point1.X;
            //Double D = Point2.Y - Point1.Y;

            //Double dot = A * C + B * D;
            //Double len_sq = C * C + D * D;
            //Double param = dot / len_sq;

            //Double xx, yy;

            //if (param < 0)
            //{
            //    xx = Point1.X;
            //    yy = Point1.Y;
            //}
            //else if (param > 1)
            //{
            //    xx = Point2.X;
            //    yy = Point2.Y;
            //}
            //else
            //{
            //    xx = Point1.X + param * C;
            //    yy = Point1.Y + param * D;
            //}

            //Double d = DistanceBetweenOn2DPlane(Point, new Point(xx, yy));
        }

        /// <summary>
        ///  Douglases the peucker reduction that works on curves that are createt by fraction of raw curves on corner points
        /// </summary>
        /// <param name="rawMypoints">Raw points</param>
        /// <param name="pdtolerance">Tolerance parameter</param>
        /// <param name="cornerAnalysedBitMap">Corner points</param>
        /// <returns>Bitmaps of curved points</returns>
        public static bool[] douglasPeuckerReduction(MyPoint[] rawMypoints, int pdtolerance, bool[] cornerAnalysedBitMap)
        {
            var partialResult = new List<int>();
            var borders = SupportFunctions.bitMapToIndex(cornerAnalysedBitMap);
            for (int i = 0; i < borders.Count - 1; i++)
            {
                var borderPoints = selectPoints(borders[i], borders[i + 1], rawMypoints);
                var partialRawResult = douglasPeuckerReduction(borderPoints, pdtolerance);
                partialResult.AddRange(partialRawResult.Select(i1 => i1 + borders[i]));
            }
            partialResult.Sort();
            return SupportFunctions.indexToBitMap(partialResult, rawMypoints.Length);
        }

        /// <summary>
        ///  Douglases the peucker reduction that works on curves that are createt by fraction of raw curves on corner points
        /// </summary>
        /// <param name="rawMypoints">Raw points</param>
        /// <param name="pdtolerance">Tolerance parameter</param>
        /// <param name="cornerAnalysedBitMap">Corner points</param>
        /// <returns>Bitmaps of curved points</returns>
        public static bool[] douglasPeuckerReductionThread(MyPoint[] rawMypoints, int pdtolerance, bool[] cornerAnalysedBitMap)
        {
            ThreadPool.SetMaxThreads(6, 6);
            var borders = SupportFunctions.bitMapToIndex(cornerAnalysedBitMap);
            _parialThreadResult = new List<int>[borders.Count];
            var complete = new CountdownEvent(1);
            //complete.TryAddCount();
            for (int i = 0; i < borders.Count - 1; i++)
            {
                var borderPoints = selectPoints(borders[i], borders[i + 1], rawMypoints);
                int i1 = i;
                complete.TryAddCount();
                ThreadPool.QueueUserWorkItem(state =>
                {
                    try
                    {
                        Debug.WriteLine(String.Format("<---Starting thread for douglas Peucker Reduction <{0} {1}> --->", borders[i1], borders[i1 + 1]));
                        douglasPeuckerReductionThread(borderPoints, pdtolerance, i1);
                    }
                    finally
                    {
                        Debug.WriteLine(String.Format("<---Finishing thread for douglas Peucker Reduction <{0} {1}> --->", borders[i1], borders[i1 + 1]));
                        lock (complete)
                            complete.Signal();
                    }
                }, null);
            }
            complete.Signal();
            complete.Wait();
            var resultList = new List<int>();
            foreach (List<int> ints in _parialThreadResult)
            {
                if (ints != null)
                    resultList.AddRange(ints);
            }
            var result = SupportFunctions.indexToBitMap(resultList, rawMypoints.Length);
            return result;
        }

        private static MyPoint[] selectPoints(int leftBorder, int rightBorder, MyPoint[] myPoints)
        {
            var result = new MyPoint[rightBorder - leftBorder];
            Array.ConstrainedCopy(myPoints, leftBorder, result, 0, rightBorder - leftBorder);
            return result;
        }
    }
}